18.8 监控
我们在前面已经介绍过好几回系统监控线程了,现在对它做个总结。
- 释放闲置超过5分钟的span物理内存。
- 如果超过2分钟没有垃圾回收,则强制执行。
- 将长时间未处理的netpoll结果添加到任务队列。
- 向长时间运行的G任务发出抢占调度。
- 收回因syscall而长时间阻塞的P。
在进入垃圾回收状态时,sysmon会自动进入休眠,所以我们才会在syscall里看到很多唤醒指令。另外,startTheWorld也会做唤醒处理。保证监控线程正常运行,对内存分配、垃圾回收和并发调度都非常重要。
proc1.go
func startTheWorldWithSema() { sched.gcwaiting = 0 if sched.sysmonwait != 0 { sched.sysmonwait = 0 notewakeup(&sched.sysmonnote) } }
现在,让我们忽略其他任务,看看对syscall和preempt的处理。
proc1.go
func sysmon() { for { usleep(delay) // STW 时休眠 sysmon if debug.schedtrace ⇐ 0 && (sched.gcwaiting != 0 || atomicload(&sched.npidle) == uint32(gomaxprocs)) { if atomicload(&sched.gcwaiting) != 0 || atomicload(&sched.npidle) == uint32(gomaxprocs) { // 设置休眠标志,休眠(有个超时,苏醒保障) atomicstore(&sched.sysmonwait, 1) notetsleep(&sched.sysmonnote, maxsleep) // 唤醒后重置状态标志,继续执行 atomicstore(&sched.sysmonwait, 0) noteclear(&sched.sysmonnote) } } lastpoll := int64(atomicload64(&sched.lastpoll)) now := nanotime() unixnow := unixnanotime() // 获取超过 10ms 的 netpoll 结果 if lastpoll != 0 && lastpoll+1010001000 < now { cas64(&sched.lastpoll, uint64(lastpoll), uint64(now)) gp := netpoll(false) // non-blocking - returns list of goroutines if gp != nil { injectglist(gp) } } // 抢夺 syscall 长时间阻塞的 P // 向长时间运行的 G 发出抢占调度 if retake(now) != 0 { idle = 0 } else { idle++ } } }
专门有个pdesc的全局变量用于保存sysmon运行统计信息,据此来判断syscall和G是否超时。
proc1.go
var pdesc [_MaxGomaxprocs]struct { schedtick uint32 schedwhen int64 syscalltick uint32 syscallwhen int64 } const forcePreemptNS = 10 * 1000 * 1000 // 10ms func retake(now int64) uint32 { // 遍历 P for i := int32(0); i < gomaxprocs; i++ { p := allp[i] pd := &pdesc[i] s := p.status // P 处于 syscall 模式 if s == _Psyscall { // 更新 syscall 统计信息 t := int64(p.syscalltick) if int64(pd.syscalltick) != t { pd.syscalltick = uint32(t) pd.syscallwhen = now continue } // 检查是否有其他任务需要 P,是否超出时间限制,是否有必要抢夺 P if runqempty(p) && atomicload(&sched.nmspinning)+atomicload(&sched.npidle) > 0 && pd.syscallwhen+1010001000 > now { continue } // 抢夺 P if cas(&p.status, s, _Pidle) { p.syscalltick++ handoffp(p) } } else if s == _Prunning { // 更新 G 运行统计信息 t := int64(p.schedtick) if int64(pd.schedtick) != t { pd.schedtick = uint32(t) pd.schedwhen = now continue } // 如果没超过 10ms,则忽略 if pd.schedwhen+forcePreemptNS > now { continue } // 发出抢占调度 preemptone(p) } } }
抢占调度
所谓抢占调度要比你想象的简单许多,远不是你以为的“抢占式多任务操作系统”那种样子。因为Go调度器并没有真正意义上的时间片概念,只是在目标G上设置一个抢占标志,当该任务调用某个函数时,被编译器安插的指令就会检查这个标志,从而决定是否暂停当前任务。
proc1.go
// Tell the goroutine running on processor P to stop. // This function is purely best-effort. It can incorrectly fail to inform the // goroutine. It can send inform the wrong goroutine. Even if it informs the // correct goroutine, that goroutine might ignore the request if it is // simultaneously executing newstack. // No lock needs to be held. // Returns true if preemption request was issued. // The actual preemption will happen at some point in the future // and will be indicated by the gp→status no longer being // Grunning func preemptone(p *p) bool { mp := p.m.ptr() gp := mp.curg gp.preempt = true // Every call in a go routine checks for stack overflow by // comparing the current stack pointer to gp→stackguard0. // Setting gp→stackguard0 to StackPreempt folds // preemption into the normal stack overflow check. gp.stackguard0 = stackPreempt return true }
保留这段代码里的注释,是想告诉你,preempt真的有些不靠谱。
有两个标志,实际起作用的是G.stackguard0。G.preempt只是后备,以便在stackguard0做回溢出检查标志时,依然可用preempt恢复抢占状态。
编译器插入的指令?没错,就是那个morestack。当它调用newstack扩容时会检查抢占标志,并决定是否暂停当前任务,当然这发生在实际扩容之前。
stack1.go
func newstack() { preempt := atomicloaduintptr(&gp.stackguard0) == stackPreempt if preempt { // 如果 M 持有锁,或者正在进行内存分配、垃圾回收等操作,不抢占,留待下次 if thisg.m.locks != 0 || thisg.m.mallocing != 0 || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning { // stackguard0 恢复溢出检查用途,下次用 G.preempt 恢复 gp.stackguard0 = gp.stack.lo + _StackGuard gogo(&gp.sched) // never return } } if preempt { // 垃圾回收本身也算一次抢占,忽略本次抢占调度 if gp.preemptscan { for !castogscanstatus(gp, _Gwaiting, _Gscanwaiting) { // Likely to be racing with the GC as // it sees a _Gwaiting and does the // stack scan. If so, gcworkdone will // be set and gcphasework will simply // return. } if !gp.gcscandone { scanstack(gp) gp.gcscandone = true } gp.preemptscan = false gp.preempt = false casfrom_Gscanstatus(gp, _Gscanwaiting, _Gwaiting) casgstatus(gp, _Gwaiting, _Grunning) gp.stackguard0 = gp.stack.lo + _StackGuard gogo(&gp.sched) // never return } // 开始抢占调度,将当前 G 放回队列,让 M 执行其他任务 casgstatus(gp, _Gwaiting, _Grunning) gopreempt_m(gp) // never return } // Allocate a bigger segment and move the stack. copystack(gp, uintptr(newsize)) gogo(&gp.sched) }
proc1.go
func gopreempt_m(gp *g) { goschedImpl(gp) } func goschedImpl(gp *g) { status := readgstatus(gp) casgstatus(gp, _Grunning, _Grunnable) dropg() globrunqput(gp) schedule() }
这个抢占调度机制给我的感觉是越来越弱,毕竟垃圾回收和栈扩容这个时机都不是很“确定”和“实时”,更何况还有函数内联和纯算法循环等造成morestack不会执行等因素。不知道对此Go的后续版本会有何改进。